{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 2.1 用大模型构建新人答疑机器人\n",
    "\n",
    "## 🚄 前言\n",
    "你在一家教育内容开发公司工作，随着新员工的持续加入，频繁的答疑需求导致了显著的时间与资源成本。你决定使用大模型技术来构建一个答疑机器人，提升答疑的准确率与效率。\n",
    "\n",
    "## 🍁 课程目标\n",
    "学完本节课程后，你将能够：\n",
    "\n",
    "* 通过API调用通义千问\n",
    "* 学习大模型的工作原理\n",
    "* 了解大模型的局限性及其解决方案\n",
    "\n",
    "## ⚠️ 环境准备 \n",
    "为了让你能够更流畅地体验教程内容，建议你首先完成 **阿里云大模型高级工程师ACP认证课程** 中的 [**环境准备**](https://edu.aliyun.com/course/3130200/lesson/343310285) 章节，并确保课程所需的环境已经正确安装。\n",
    "\n",
    "\n",
    "## 💻 1. 通过API调用通义千问\n",
    "\n",
    "体验大模型最直接的方式就是通过网页界面（如[通义千问](https://tongyi.aliyun.com/qianwen/)）与它对话。不过作为开发者，往往需要在自己的应用中集成大模型能力，你可以使用业界广泛采用的 OpenAI Python SDK 来调用通义千问大模型。你已经在`1.0 计算环境准备`安装了必要的依赖，在执行接下来的代码前，确认已经切换到新创建的python环境，如本例中创建的 `Python (llm_learn)`。\n",
    "\n",
    "<img src=\"https://img.alicdn.com/imgextra/i4/O1CN01nf2EYI1pwhhMbOWHe_!!6000000005425-0-tps-2258-1004.jpg\" width=\"600\">\n",
    "\n",
    "<img src=\"https://img.alicdn.com/imgextra/i3/O1CN01rn0jvB1Z1QJXUWaG2_!!6000000003134-0-tps-3138-914.jpg\" width=\"600\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "为了调用通义千问，你需要前往阿里云大模型服务平台百炼，开通 [模型调用服务](https://bailian.console.aliyun.com/#/model-market) 并 [创建一个API Key](https://bailian.console.aliyun.com/?apiKey=1#/api-key)。\n",
    "\n",
    "> 如果页面顶部显示如下，则表示你尚未开通百炼的模型调用服务。开通服务后可以调用模型。\n",
    "> <img src=\"https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/5298748271/p856749.png\" width=\"600\">\n",
    "\n",
    "在使用 API 之前，你需要妥善处理 API Key 的安全问题。直接在代码中写入 API Key 是一个不好的习惯，因为这样很容易在分享代码时泄露密钥，并且在更换API Key后所有明文编码的API Key部分都需要修改。更安全、便捷的做法是将 API Key 存储在环境变量中。\n",
    "\n",
    "下面的代码会从配置文件中加载你的 API Key，并将其设置为环境变量。代码执行后会显示 API Key 的前五位字符（后面用星号代替），这样你可以确认配置是否正确，同时又不会暴露完整的密钥。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T08:58:03.667571Z",
     "iopub.status.busy": "2024-11-15T08:58:03.667184Z",
     "iopub.status.idle": "2024-11-15T08:59:44.060371Z",
     "shell.execute_reply": "2024-11-15T08:59:44.059650Z",
     "shell.execute_reply.started": "2024-11-15T08:58:03.667545Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "你配置的 API Key 是：sk-88*****\n"
     ]
    }
   ],
   "source": [
    "# 加载百炼的 API Key 用于调用通义千问大模型\n",
    "import os\n",
    "from config.load_key import load_key\n",
    "load_key()\n",
    "print(f'''你配置的 API Key 是：{os.environ[\"DASHSCOPE_API_KEY\"][:5]+\"*\"*5}''')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "先来尝试一个简单的对话。下面的代码创建了一个名为“公司小蜜”的助手，它可以回答关于公司运营的问题。你可以使用“选择项目管理工具”这个常见问题作为示例："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T08:59:46.180449Z",
     "iopub.status.busy": "2024-11-15T08:59:46.179984Z",
     "iopub.status.idle": "2024-11-15T08:59:59.498380Z",
     "shell.execute_reply": "2024-11-15T08:59:59.497544Z",
     "shell.execute_reply.started": "2024-11-15T08:59:46.180423Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "选择项目管理工具时，需要考虑团队的规模、项目的特点以及团队成员的习惯等因素。以下是一些常见的项目管理工具，供您参考：\n",
      "\n",
      "1. **Trello**：适合小型团队和灵活的项目管理。它基于看板方式管理任务，非常适合敏捷开发流程。\n",
      "\n",
      "2. **Jira**：对于软件开发团队来说，Jira 是一个非常强大的工具。它支持敏捷开发、问题跟踪和项目跟踪等功能，非常适合技术团队使用。\n",
      "\n",
      "3. **Asana**：适用于各种规模的团队，提供任务分配、进度跟踪、文件共享等多功能，帮助团队高效协作。\n",
      "\n",
      "4. **Microsoft Project**：适合大型企业和复杂项目的管理。它提供了详细的计划制定、资源分配、成本控制等功能。\n",
      "\n",
      "5. **Monday.com**：这是一个非常直观且用户友好的平台，支持自定义工作流，适合不同类型的项目和团队使用。\n",
      "\n",
      "6. **Wrike**：提供项目规划、时间线视图、团队协作等功能，适合需要跨部门合作的项目。\n",
      "\n",
      "7. **Notion**：虽然Notion最初是一个笔记应用，但它也提供了项目管理和团队协作的功能，非常适合那些希望将所有工作相关的信息整合到一个平台上的团队。\n",
      "\n",
      "建议您可以根据团队的具体需求，试用几个不同的工具，看看哪个最符合您的工作流程和团队习惯。每个工具都有免费试用期，这可以帮助您更好地了解它们是否适合您的团队。\n"
     ]
    }
   ],
   "source": [
    "from openai import OpenAI\n",
    "import os\n",
    "client = OpenAI(\n",
    "    api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n",
    "    base_url=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n",
    ")\n",
    "def get_qwen_response(prompt):\n",
    "    response = client.chat.completions.create(\n",
    "        model=\"qwen-plus-0919\",\n",
    "        messages=[\n",
    "            # system message 用于设置大模型的角色和任务\n",
    "            {\"role\": \"system\", \"content\": \"你负责教育内容开发公司的答疑，你的名字叫公司小蜜，你要回答同事们的问题。\"},\n",
    "            # user message 用于输入用户的问题\n",
    "            {\"role\": \"user\", \"content\": prompt}\n",
    "        ]\n",
    "    )\n",
    "    return response.choices[0].message.content\n",
    "response = get_qwen_response(\"我们公司项目管理应该用什么工具\")\n",
    "print(response)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> 如果你想实现多轮对话（让大模型参考历史对话信息进行回复），可以参考[多轮对话](https://help.aliyun.com/zh/model-studio/user-guide/text-generation#865b38621dwin)。\n",
    "\n",
    "运行上面的代码后，你会注意到需要等待一段时间（约20秒）才能看到完整的回复。这是因为默认情况下，API 会等待模型生成完所有内容后才一次性返回结果。在实际应用中，这种等待可能会影响用户体验 —— 想象一下用户盯着一个空白界面等待20秒的场景！\n",
    "\n",
    "幸运的是，你可以使用\"流式输出\"（Streaming）来优化这个问题。使用流式输出时，模型会像人类打字一样，一边思考一边输出，让用户能够立即看到部分回复，大大提升交互体验。接下来，看看如何实现流式输出...\n",
    "> 💡 小贴士：流式输出只是改变了内容的展示方式，模型的思考过程和最终答案的质量都保持不变。你可以放心使用这个功能来优化你的应用体验。\n",
    "\n",
    "要实现流式输出，只需在之前的代码基础上添加 `stream=True` 参数，并调整输出方式："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T09:00:04.752657Z",
     "iopub.status.busy": "2024-11-15T09:00:04.752276Z",
     "iopub.status.idle": "2024-11-15T09:00:18.769987Z",
     "shell.execute_reply": "2024-11-15T09:00:18.769374Z",
     "shell.execute_reply.started": "2024-11-15T09:00:04.752633Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "选择项目管理工具时，需要考虑团队的具体需求、项目的规模和类型以及预算等因素。目前市面上有许多优秀的项目管理工具，以下是一些常见的选项：\n",
      "\n",
      "1. **Trello**：适合小型项目或团队使用，界面直观，操作简单。通过卡片和列表的形式来组织任务，非常适合敏捷开发。\n",
      "\n",
      "2. **Jira**：对于软件开发团队来说是一个非常好的选择，支持敏捷开发方法，功能强大，可以自定义工作流，但相对复杂一些，可能需要一定时间来熟悉。\n",
      "\n",
      "3. **Asana**：适用于各种规模的团队，提供了丰富的功能来帮助团队规划、跟踪和管理项目。界面友好，易于上手。\n",
      "\n",
      "4. **Microsoft Project**：适合大型企业和复杂的项目管理，功能全面，但学习曲线较陡，价格也较高。\n",
      "\n",
      "5. **Teambition**（团队版）：国内用户可以选择，提供任务管理、文件共享、日程安排等一体化服务，操作简便，适合中小型企业使用。\n",
      "\n",
      "6. **禅道Zentao**：专为软件研发团队设计的开源项目管理软件，集成了产品管理、项目管理、质量管理等功能，支持敏捷开发模式。\n",
      "\n",
      "建议根据团队的实际需求和偏好，试用几款不同的工具后做出选择。希望这些建议对您有所帮助！如果还有其他问题，欢迎随时向我咨询。"
     ]
    }
   ],
   "source": [
    "def get_qwen_stream_response(user_prompt,system_prompt):\n",
    "    response = client.chat.completions.create(\n",
    "        model=\"qwen-plus-0919\",\n",
    "        messages=[\n",
    "            {\"role\": \"system\", \"content\": system_prompt},\n",
    "            {\"role\": \"user\", \"content\": user_prompt}\n",
    "        ],\n",
    "        stream=True\n",
    "    )\n",
    "    for chunk in response:\n",
    "        yield chunk.choices[0].delta.content\n",
    "\n",
    "response = get_qwen_stream_response(user_prompt=\"我们公司项目管理应该用什么工具\",system_prompt=\"你负责教育内容开发公司的答疑，你的名字叫公司小蜜，你要回答同事们的问题。\")\n",
    "for chunk in response:\n",
    "    print(chunk, end=\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "通过对通义千问模型的两次提问，你可能会发现一些有趣的现象：\n",
    "1. 即使问题完全相同，每次的回答都略有不同。这是大模型的特性之一，就像人类一样，它会用不同的方式表达相似的观点。\n",
    "2. 模型给出的建议集中在一些通用的项目管理工具上，比如 Jira、Trello 或 Asana。虽然大模型知识丰富，但它并不了解你公司的具体情况，比如已有的工具链、团队规模、预算限制等。\n",
    "> 公司使用的项目管理软件资料可以从docs/内容开发工程师岗位指导说明书.pdf文件中找到。\n",
    "\n",
    "这两个现象其实很有意思！为什么大模型会表现出这样的特点呢？要理解这一点，需要掀开大模型的“神秘面纱”，看看它是如何思考和工作的。别担心，你将通过简单直观的方式来理解这些概念。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 📚 2. 大模型是如何工作的\n",
    "近几十年来，人工智能经历了从基础算法到生成式AI的深刻演变。生成式AI通过学习大量数据可以创造出全新的内容，如文本、图像、音频和视频，这极大地推动了AI技术的广泛应用。常见的应用场景包括智能问答（如通义千问、GPT）、创意作画（如Stable Diffusion）以及代码生成（如通义灵码）等，涵盖了各个领域，让AI触手可及。\n",
    "\n",
    "<img src=\"https://img.alicdn.com/imgextra/i3/O1CN01XhNFzh1bj3EybLhgk_!!6000000003500-0-tps-2090-1138.jpg\" width=\"600\">\n",
    "\n",
    "智能问答作为大模型最经典且广泛的应用之一，是我们探索大模型工作机制的最佳范例。接下来将介绍大模型在问答场景中的工作流程，帮助你更深入地理解其背后的技术原理。\n",
    "\n",
    "### 2.1. 大模型的问答工作流程\n",
    "下面以“ACP is a very”为输入文本向大模型发起一个提问，下图展示从发起提问到输出文本的完整流程。\n",
    "\n",
    "<img src=\"https://img.alicdn.com/imgextra/i4/O1CN017JNKTk22p00d1nFKk_!!6000000007168-0-tps-2458-1194.jpg\" width=\"600\">\n",
    "\n",
    "大模型的问答工作流程有以下五个阶段：\n",
    "\n",
    "**第一阶段：输入文本分词化**\n",
    "\n",
    "分词（Token）是大模型处理文本的基本单元，通常是词语、词组或者符号。我们需要将“ACP is a very”这个句子分割成更小且具有独立语义的词语（Token），并且为每个Token分配一个ID。如有需要，你可以使用[Tokenizer API](https://help.aliyun.com/zh/dashscope/developer-reference/token-api?spm=5176.28197632.0.0.2130607dUIVd7Y&disableWebsiteRedirect=true)计算Token。\n",
    "\n",
    "<img src=\"https://gw.alicdn.com/imgextra/i1/O1CN019gAS3k1DrhwpIdHl6_!!6000000000270-0-tps-2414-546.jpg\" width=\"600\">\n",
    "\n",
    "**第二阶段：Token向量化**\n",
    "\n",
    "计算机只能理解数字，无法直接理解Token的含义。因此需要将Token进行数字化转换（即转化为向量），使其可以被计算机所理解。Token向量化会将每个Token转化为固定维度的向量。\n",
    "\n",
    "\n",
    "**第三阶段：大模型推理**\n",
    "\n",
    "大模型通过大量已有的训练数据来学习知识，当我们输入新内容，比如“ACP is a very”时，大模型会结合所学知识进行推测。它会计算所有可能Token的概率，得到候选Token的概率集合。最后，大模型通过计算选出一个Token作为下一个输出。\n",
    "\n",
    "这就解释了为什么当询问公司的项目管理工具时，模型无法提供内部工具的建议，这是因为其推测能力是基于已有的训练数据，对它未接触的知识无法给出准确的回答。因此，在需要答疑机器人回答私域知识时，需要针对性地解决这一问题，在本小节第3部分会进一步阐述。\n",
    "\n",
    "**第四阶段：输出Token**\n",
    "\n",
    "由于大模型会根据候选Token的概率进行随机挑选，这就会导致“即使问题完全相同，每次的回答都略有不同”。为了控制生成内容的随机性，目前普遍是通过temperature和top_p来调整的。\n",
    "\n",
    "例如，下图中大模型输出的第一个候选Token集合为“informative（50%）”、“fun（25%）”、“enlightening（20%）”、“boring（5%）”。通过调整temperature或top_p参数，将影响大模型在候选Token集合中的选择倾向，如选择概率最高的“informative”。你可以在本小节 2.2 中进一步了解这两个参数。\n",
    "\n",
    "<img src=\"https://img.alicdn.com/imgextra/i3/O1CN01N93ZE81e6zAZA4TiK_!!6000000003823-0-tps-582-340.jpg\" width=\"300\">\n",
    "\n",
    "特别地，“informative”会被继续送入大模型，用于生成候选Token。这个过程被称为自回归，它会利用到输入文本和已生成文本的信息。大模型采用这种方法依次生成候选Token。\n",
    "\n",
    "**第五阶段：输出文本**\n",
    "\n",
    "循环第三阶段和第四阶段的过程，直到输出特殊Token（如<EOS>，end of sentence，即“句子结束”标记）或输出长度达到阈值，从而结束本次问答。大模型会将所有生成的内容输出。当然你可以使用大模型的流式输出能力，即预测一些Token立即进行返回。这个例子最终会输出“ACP is a very informative course.”。\n",
    "    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.2 影响大模型内容生成的随机性参数\n",
    "\n",
    "假设在一个对话问答场景中，用户提问为：“在大模型ACP课程中，你可以学习什么？”。为了模拟大模型生成内容的过程，我们预设了一个候选Token集合，这些Token分别为：“RAG”、“提示词”、“模型”、“写作”、“画画”。大模型会从这5个候选Token中选择一个作为结果输出（next-token），如下所示。\n",
    "> 用户提问：在大模型ACP课程中，你可以学习什么？<br><br>\n",
    "> 大模型回答：RAG <br>\n",
    "\n",
    "在这个过程中，有两个重要参数会影响大模型的输出：temperature 和 top_p，它们用来控制大模型生成内容的随机性和多样性。下面介绍这两个参数的工作原理和使用方式。\n",
    "\n",
    "#### 2.2.1 temperature：调整候选Token集合的概率分布\n",
    "\n",
    "在大模型生成下一个词（next-token）之前，它会先为候选Token计算一个初始概率分布。这个分布表示每个候选Token作为next-token的概率。temperature是一个调节器，它通过改变候选Token的概率分布，影响大模型的内容生成。通过调节这个参数，你可以灵活地控制生成文本的多样性和创造性。\n",
    "\n",
    "为了更直观地理解，下图展示了不同temperature值对候选Token概率分布的影响。绘图代码放置在 /resources/2_1 文件目录下。\n",
    "\n",
    "<img src=\"https://img.alicdn.com/imgextra/i4/O1CN0137QeqL1o3uhFmaXHU_!!6000000005170-0-tps-3538-1242.jpg\" width=\"1000\">\n",
    "\n",
    "图中的低、中、高温度基于通义千问Plus模型的范围[0, 2)划分。\n",
    "\n",
    "由上图可知，温度从低到高（0.1 -> 0.7 -> 1.2），概率分布从陡峭趋于平滑，候选Token“RAG”从出现的概率从0.8 -> 0.6 -> 0.3，虽然依然是出现概率最高的，但是已经和其它的候选Token概率接近了，最终输出也会从相对固定到逐渐多样化。\n",
    "\n",
    "针对不同使用场景，可参考以下建议设置 temperature 参数：\n",
    "- 明确答案（如生成代码）：调低温度。\n",
    "- 创意多样（如广告文案）：调高温度。\n",
    "- 无特殊需求：使用默认温度（通常为中温度范围）。\n",
    "\n",
    "需要注意的是，当 temperature=0 时，虽然会最大限度降低随机性，但无法保证每次输出完全一致。如果想深入了解，可查阅 [temperature的底层算法实现](https://github.com/huggingface/transformers/blob/v4.49.0/src/transformers/generation/logits_process.py#L226)。\n",
    "\n",
    "下面体验temperature的效果。通过调整temperature值，对同一问题提问 10 次，观察回答内容的波动情况。\n",
    "> temperature的示例代码跟接下来要讲解的top_p类似，先封装以供后续使用。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecutionIndicator": {
     "show": true
    },
    "execution": {
     "iopub.execute_input": "2024-11-15T12:06:39.470715Z",
     "iopub.status.busy": "2024-11-15T12:06:39.470351Z",
     "iopub.status.idle": "2024-11-15T12:06:44.434877Z",
     "shell.execute_reply": "2024-11-15T12:06:44.434229Z",
     "shell.execute_reply.started": "2024-11-15T12:06:39.470689Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "输出 1 : 驷骥\n",
      "输出 2 : 驷、驹、駣、骉。\n",
      "输出 3 : 千里马。\n",
      "输出 4 : 骏马\n",
      "输出 5 : 驹子\n",
      "输出 6 : 骏马。\n",
      "输出 7 : 千里马。\n",
      "输出 8 : 驹子\n",
      "输出 9 : 千里马。\n",
      "输出 10 : 骏马\n"
     ]
    }
   ],
   "source": [
    "import time\n",
    "\n",
    "def get_qwen_stream_response(user_prompt, system_prompt, temperature, top_p):\n",
    "    response = client.chat.completions.create(\n",
    "        model=\"qwen-plus-0919\",\n",
    "        messages=[\n",
    "            {\"role\": \"system\", \"content\": system_prompt},\n",
    "            {\"role\": \"user\", \"content\": user_prompt}\n",
    "        ],\n",
    "        temperature=temperature,\n",
    "        top_p=top_p,\n",
    "        stream=True\n",
    "    )\n",
    "    \n",
    "    for chunk in response:\n",
    "        yield chunk.choices[0].delta.content\n",
    "\n",
    "# temperature，top_p的默认值使用通义千问plus模型的默认值\n",
    "def print_qwen_stream_response(user_prompt, system_prompt, temperature=0.7, top_p=0.8, iterations=10):\n",
    "    for i in range(iterations):\n",
    "        print(f\"输出 {i + 1} : \", end=\"\")\n",
    "        ## 防止限流，添加延迟\n",
    "        time.sleep(0.5)\n",
    "        response = get_qwen_stream_response(user_prompt, system_prompt, temperature, top_p)\n",
    "        output_content = ''\n",
    "        for chunk in response:\n",
    "            output_content += chunk\n",
    "        print(output_content)\n",
    "\n",
    "# 通义千问plus模型：temperature的取值范围是[0, 2)，默认值为0.7\n",
    "# 设置temperature=0\n",
    "print_qwen_stream_response(user_prompt=\"马也可以叫做\", system_prompt=\"请帮我续写内容，字数要求是4个汉字以内。\", temperature=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T12:06:45.417531Z",
     "iopub.status.busy": "2024-11-15T12:06:45.417146Z",
     "iopub.status.idle": "2024-11-15T12:06:50.629069Z",
     "shell.execute_reply": "2024-11-15T12:06:50.628320Z",
     "shell.execute_reply.started": "2024-11-15T12:06:45.417506Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "输出 1 : 驹子\n",
      "输出 2 : 马儿、骏马、奔马。\n",
      "输出 3 : 驷骊\n",
      "输出 4 : 驷骥骦驹\n",
      "输出 5 : 驷驹。\n",
      "输出 6 : 驹子\n",
      "输出 7 : 赤兔\n",
      "输出 8 : 驷马\n",
      "输出 9 : 四蹄动物\n",
      "输出 10 : 驷子\n"
     ]
    }
   ],
   "source": [
    "# 设置temperature=1.9\n",
    "print_qwen_stream_response(user_prompt=\"马也可以叫做\", system_prompt=\"请帮我续写内容，字数要求是4个汉字以内。\", temperature=1.9)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "jupyter": {
     "outputs_hidden": true
    },
    "tags": []
   },
   "source": [
    "从实验中可以明显观察到，温度值越高，模型生成的内容更具变化和多样性。\n",
    "\n",
    "#### 2.2.2 top_p：控制候选Token集合的采样范围\n",
    "\n",
    "top_p 是一种筛选机制，用于从候选 Token 集合中选出符合特定条件的“小集合”。具体方法是：按概率从高到低排序，选取累计概率达到设定阈值的 Token 组成新的候选集合，从而缩小选择范围。\n",
    "\n",
    "下图展示了不同top_p值对候选Token集合的采样效果。\n",
    "\n",
    "<img src=\"https://img.alicdn.com/imgextra/i1/O1CN01xmkonv21sNL6VtQpi_!!6000000007040-0-tps-2732-1282.jpg\" width=\"850\">\n",
    "\n",
    "图示中蓝色部分表示累计概率达到top_p阈值（如0.5或0.9）的Token，它们组成新的候选集合；灰色部分则是未被选中的Token。\n",
    "\n",
    "当top_p=0.5时，模型优先选择最高概率的Token，即“RAG”；而当top_p=0.9时，模型会在“RAG”、“提示词”、“模型”这三个Token中随机选择一个生成输出。\n",
    "\n",
    "\n",
    "由此可见，top_p值对大模型生成内容的影响可总结为：\n",
    "- 值越大 ：候选范围越广，内容更多样化，适合创意写作、诗歌生成等场景。\n",
    "- 值越小 ：候选范围越窄，输出更稳定，适合新闻初稿、代码生成等需要明确答案的场景。\n",
    "- 极小值（如 0.0001）：理论上模型只选择概率最高的 Token，输出非常稳定。但实际上，由于分布式系统、模型输出的额外调整等因素可能引入的微小随机性，仍无法保证每次输出完全一致。\n",
    "\n",
    "\n",
    "下面体验top_p的效果。通过调整top_p值，对同一问题提问10次，观察回答内容的波动情况。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T12:06:52.894507Z",
     "iopub.status.busy": "2024-11-15T12:06:52.894139Z",
     "iopub.status.idle": "2024-11-15T12:06:58.171739Z",
     "shell.execute_reply": "2024-11-15T12:06:58.171137Z",
     "shell.execute_reply.started": "2024-11-15T12:06:52.894481Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "输出 1 : \"智游无界\"\n",
      "输出 2 : \"智游无界\"\n",
      "输出 3 : \"智游无界\"\n",
      "输出 4 : \"智游无界\"\n",
      "输出 5 : \"智游无界\"\n",
      "输出 6 : \"智游无界\"\n",
      "输出 7 : \"智游无界\"\n",
      "输出 8 : \"智游无界\"\n",
      "输出 9 : \"智游无界\"\n",
      "输出 10 : \"智游无界\"\n"
     ]
    }
   ],
   "source": [
    "# 通义千问plus模型：top_p取值范围为(0,1]，默认值为0.8。\n",
    "# 设置top_p=0.001\n",
    "print_qwen_stream_response(user_prompt=\"为一款智能游戏手机取名，可以是\", system_prompt=\"请帮我取名，字数要求是4个汉字以内。\", top_p=0.001)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2024-11-15T12:06:58.394593Z",
     "iopub.status.busy": "2024-11-15T12:06:58.394205Z",
     "iopub.status.idle": "2024-11-15T12:07:03.378361Z",
     "shell.execute_reply": "2024-11-15T12:07:03.377556Z",
     "shell.execute_reply.started": "2024-11-15T12:06:58.394567Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "输出 1 : \"智游无界\"\n",
      "输出 2 : \"智游无界\"\n",
      "输出 3 : \"智胜游戏王\"\n",
      "输出 4 : \"智极竞界\"\n",
      "输出 5 : \"智炫游戏王\"\n",
      "输出 6 : \"智游无界\"\n",
      "输出 7 : \"智游无界\"\n",
      "输出 8 : \"智游无界\"\n",
      "输出 9 : \"极智游龙\"\n",
      "输出 10 : \"智游无界\"\n"
     ]
    }
   ],
   "source": [
    "# 设置top_p=0.9\n",
    "print_qwen_stream_response(user_prompt=\"为一款智能游戏手机取名，可以是\",system_prompt=\"请帮我取名，字数要求是4个汉字以内。\", top_p=0.9)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "根据实验结果可以发现，top_p值越高，大模型的输出结果随机性越高。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 2.2.3 小结 \n",
    "\n",
    "**是否需要同时调整temperature和top_p？**\n",
    "\n",
    "为了确保生成内容的可控性，建议不要同时调整top_p和temperature，同时调整可能导致输出结果不可预测。你可以优先调整其中一种参数，观察其对结果的影响，再逐步微调。\n",
    "\n",
    "><br>**知识延展：top_k**<br>\n",
    "在通义千问系列模型中，参数top_k也有类似top_p的能力，可查阅[通义千问API文档](https://help.aliyun.com/zh/model-studio/developer-reference/use-qwen-by-calling-api?spm=a2c4g.11186623.help-menu-2400256.d_3_3_0.68332bdb2Afk2s&scm=20140722.H_2712576._.OR_help-V_1)。它是一种采样机制，从概率排名前k的Token中随机选择一个进行输出。一般来说，top_k越大，生成内容越多样化；top_k越小，内容则更固定。当top_k设置为1时，模型仅选择概率最高的Token，输出会更加稳定，但也会导致缺乏变化和创意。<br><br>\n",
    ">**知识延展：seed**<br>\n",
    "在通义千问系列模型中，参数seed也支持控制生成内容的确定性，可查阅[通义千问API文档](https://help.aliyun.com/zh/model-studio/developer-reference/use-qwen-by-calling-api?spm=a2c4g.11186623.help-menu-2400256.d_3_3_0.68332bdb2Afk2s&scm=20140722.H_2712576._.OR_help-V_1)。在每次模型调用时传入相同的seed值，并保持其他参数不变，模型会尽最大可能返回相同结果，但无法保证每次结果完全一致。<br><br>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**设置 temperature、top_p、seed 控制大模型输出，为何仍存在随机性？**\n",
    "\n",
    "即使将 temperature 设置为 0、top_p 设置为极小值（如 0.0001），并使用相同的 seed ，同一个问题的生成结果仍可能出现不一致。这是因为一些复杂因素可能引入微小的随机性，例如大模型运行在分布式系统中，或模型输出引入了优化。\n",
    "\n",
    "举个例子：\n",
    "分布式系统就像用不同的机器切面包。虽然每台机器都按照相同的设置操作，但由于设备之间的细微差异，切出来的面包片可能还是会略有不同。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ⚙️ 3. 让大模型能够回答私域知识问题\n",
    "为了让大模型能够回答私域知识问题，你可以选择以下两种方案：\n",
    "- **不改变模型**<br>\n",
    "    在提问时，直接传入私域知识相关的参考信息。\n",
    "- **改变模型**<br>\n",
    "    通过微调甚至训练一个新的模型，使其能够更好地理解和回答特定领域的问题。\n",
    "    \n",
    "考虑到微调和训练新模型的高成本，在私有知识问答场景中，你可以优先考虑通过提示词传递私域知识。这种方法更加简便且高效。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "你好！你是软件研发一组的成员，根据公司目前的规定，你们组还在使用 **Jira** 作为项目管理工具。不过请注意，公司计划在2026年之前逐步将所有团队切换到 **Microsoft Project**。\n",
      "\n",
      "如果你有任何关于 Jira 使用上的问题，或者需要帮助进行未来的切换准备，随时可以联系我。希望这能帮到你！"
     ]
    }
   ],
   "source": [
    "# 用户问题\n",
    "user_question = \"我是软件一组的，请问项目管理应该用什么工具\"\n",
    "\n",
    "# 公司项目管理工具相关的知识\n",
    "knowledge = \"\"\"公司项目管理工具有两种选择：\n",
    "  1. **Jira**：对于软件开发团队来说，Jira 是一个非常强大的工具，支持敏捷开发方法，如Scrum和Kanban。它提供了丰富的功能，包括问题跟踪、时间跟踪等。\n",
    "\n",
    "  2. **Microsoft Project**：对于大型企业或复杂项目，Microsoft Project 提供了详细的计划制定、资源分配和成本控制等功能。它更适合那些需要严格控制项目时间和成本的场景。\n",
    "  \n",
    "  在一般情况下请使用Microsoft Project，公司购买了完整的许可证。软件研发一组、三组和四组正在使用Jira，计划于2026年之前逐步切换至Microsoft Project。\n",
    "\"\"\"\n",
    "\n",
    "response = get_qwen_stream_response(\n",
    "    user_prompt=user_question,\n",
    "    # 将公司项目管理工具相关的知识作为背景信息传入系统提示词\n",
    "    system_prompt=\"你负责教育内容开发公司的答疑，你的名字叫公司小蜜，你要回答学员的问题。\"+ knowledge,\n",
    "    temperature=0.7,\n",
    "    top_p=0.8\n",
    ")\n",
    "\n",
    "for chunk in response:\n",
    "    print(chunk, end=\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "在提问时传入参考信息后，大模型就能回答私有知识问题了。然而，这种方法的缺点也很明显：提示词长度有限，当私有数据量过大时，传入所有背景信息可能导致提示词过长，从而影响模型的处理效率或达到长度上限。\n",
    "\n",
    "为了解决这个问题，你可以在用户提问时，自动检索相关的私有知识，并将检索到的文档片段与用户输入合并后传给大模型，从而生成最终的答案。这样可以避免在提示词中直接传入大量背景信息。这种实现方案，也被称为检索增强式生成（RAG，Retrieval-Augmented Generation）。\n",
    "\n",
    "构建一个 RAG 应用通常会分为两个阶段：\n",
    "\n",
    "### 3.1 建立索引阶段\n",
    "<img src=\"https://gw.alicdn.com/imgextra/i2/O1CN010zLf411zVoZQ9cWsI_!!6000000006720-2-tps-1592-503.png\" width=\"600\">\n",
    "\n",
    "建立索引是为了将私有知识文档或片段转换为可以高效检索的形式，通过将文件内容分割并转化为多维向量（使用专用 Embedding 模型），并结合向量存储保留文本的语义信息，方便进行相似度计算。向量化使得模型能够高效检索和匹配相关内容，特别是在处理大规模知识库时，显著提高了查询的准确性和响应速度。\n",
    "\n",
    "这些向量经过 Embedding 模型处理后不仅很好地捕捉文本内容的语义信息，而且由于语义已经向量化，标准化，便于之后与检索语义向量进行相关度计算。\n",
    "\n",
    "### 3.2 检索与生成阶段\n",
    "<img src=\"https://img.alicdn.com/imgextra/i1/O1CN01vbkBXC1HQ0SBrC1Ii_!!6000000000751-2-tps-1776-639.png\" width=\"600\">\n",
    "\n",
    "检索生成是根据用户的提问，从索引中检索相关的文档片段，这些片段会与提问一起输入到大模型生成最终的回答。这样大模型就能够回答私有知识问题了。\n",
    "\n",
    "总的来说，基于RAG 结构的应用，既避免了将整个参考文档作为背景信息输入而导致的各种问题，又通过检索提取出了与问题最相关的部分，从而提高了大模型输出的准确性与相关性。\n",
    "\n",
    "## 📝 4. 本节小结\n",
    "在本节课程中，我们学习了以下内容：\n",
    "\n",
    "- **如何使用大模型 API**<br>\n",
    "    通过实际的代码示例，了解如何使用 API 调用大模型，体验其在问答任务中的能力。\n",
    "- **初步了解大模型的工作原理**<br>\n",
    "    了解大模型如何理解问题，并生成响应，同时探讨了大模型的随机性和知识范围的限制，以及如何弥补这些不足。\n",
    "    \n",
    "除了本节课程中的示例展示的任务之外，你还可以让大模型完成更多类型的任务，如内容生成、结构化信息提取、文本分类、情感分析等。同时在你的大模型应用中引入 RAG 方案能够扩展大模型所能处理的知识范围，下一小节我们将介绍创建RAG应用的方法。\n",
    "\n",
    "### 扩展阅读\n",
    "- 在学习本课程的时候，如果你想了解更多相关概念和原理，可以尝试让大模型进一步展开说明或给出学习建议：\n",
    "> 通义千问支持开启 enable_search 参数，这个参数可以让大模型在生成回答时利用互联网搜索结果来丰富其回复内容。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "2024年巴黎奥运会，中国代表团共获得**40枚金牌**，这一成绩与美国并列金牌榜第一，同时创造了中国境外参加奥运会的最佳参赛纪录。\n"
     ]
    }
   ],
   "source": [
    "completion = client.chat.completions.create(\n",
    "    model=\"qwen-plus\",  # 此处以qwen-plus为例，可按需更换模型名称。模型列表：https://help.aliyun.com/zh/model-studio/getting-started/models\n",
    "    messages=[\n",
    "        {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n",
    "        {\"role\": \"user\", \"content\": \"巴黎奥运会中国队金牌数\"},\n",
    "    ],\n",
    "    extra_body={\"enable_search\": True},\n",
    ")\n",
    "print(completion.choices[0].message.content)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "阿里云的推理模型 [QwQ](https://help.aliyun.com/zh/model-studio/user-guide/qwq) 具有强大的推理能力，模型会先输出思考过程，再输出回答内容。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "====================思考过程====================\n",
      "\n",
      "嗯，用户问的是9.9和9.11谁大。首先，我需要确认用户的问题是关于数字比较的，也就是比较这两个小数的大小。看起来用户可能在数学题或者日常生活中遇到了这样的问题，需要确定哪个数字更大。\n",
      "\n",
      "首先，我应该回忆一下比较小数的方法。通常，比较小数的时候，先比较整数部分，如果整数部分不同，整数部分大的数就大。如果整数部分相同，再比较小数部分的第一位，也就是十分位，如果还相同，再比较百分位，依此类推，直到找到不同的位数为止。\n",
      "\n",
      "现在来看这两个数，9.9和9.11。首先比较它们的整数部分，都是9，所以整数部分相同。接下来需要比较小数部分。第一个数是9.9，小数部分只有一位，也就是十分位是9，而第二个数是9.11，小数部分有两位，十分位是1，百分位是1。\n",
      "\n",
      "这时候，可能需要把两个数的小数部分对齐，比如把9.9写成9.90，这样比较起来更直观。这样，9.90和9.11在十分位上分别是9和1，显然9比1大，所以9.90的十分位更大，因此整个数更大。所以结论应该是9.9比9.11大。\n",
      "\n",
      "不过，我需要确认用户是否有其他可能的误解。比如，是否可能用户把9.11理解成9月11日这样的日期，但问题里明确问的是“谁大”，应该还是指数值大小。另外，是否有其他可能的解释，比如货币单位？比如9.9元和9.11元，这时候比较方式还是一样的，数值大的就是金额大的。\n",
      "\n",
      "再仔细检查一下，9.9和9.11的小数位数不同，这时候可能有人会误以为小数位数多的数更大，但其实应该看每一位的数值。例如，9.11的十分位是1，而9.9的十分位是9，所以即使后面还有百分位，但十分位已经决定了大小。因此，9.9确实更大。\n",
      "\n",
      "另外，可能需要考虑用户是否对小数点后的位数有误解，比如认为9.11中的11是两位数，所以比9.9的9大，但其实每一位都要单独比较。所以正确的比较方法是先十分位，再百分位，而不是直接比较小数部分的总长度。\n",
      "\n",
      "总结一下，正确的步骤是：\n",
      "1. 比较整数部分，都是9，相同。\n",
      "2. 比较小数点后的第一位，9.9的十分位是9，而9.11的十分位是1，所以9.9的十分位更大。\n",
      "3. 因此，9.9比9.11大。\n",
      "\n",
      "可能用户会疑惑为什么9.11看起来有更多的数字，但需要明确指出比较时是按位数逐位比较，而不是总长度。所以最终结论是9.9更大。\n",
      "====================完整回复====================\n",
      "\n",
      "在比较9.9和9.11的大小时，可以按照以下步骤进行分析：\n",
      "\n",
      "1. **比较整数部分**：  \n",
      "   两个数的整数部分都是 **9**，因此整数部分相等。\n",
      "\n",
      "2. **比较小数部分**：  \n",
      "   - **9.9** 的小数部分是 **0.9**（即十分位为 **9**，百分位为 **0**）。  \n",
      "   - **9.11** 的小数部分是 **0.11**（即十分位为 **1**，百分位为 **1**）。  \n",
      "\n",
      "3. **逐位比较小数部分**：  \n",
      "   - **十分位**：  \n",
      "     9.9 的十分位是 **9**，而9.11的十分位是 **1**。  \n",
      "     显然 **9 > 1**，因此 **9.9 的十分位更大**。  \n",
      "     **无需比较百分位**，因为十分位已经决定了大小。\n",
      "\n",
      "### 结论：  \n",
      "**9.9 比 9.11 大**。\n",
      "\n",
      "---\n",
      "\n",
      "### 补充说明：  \n",
      "即使9.11的小数部分有两位（百分位为1），但由于十分位（第一位小数）的数值更小（1 < 9），因此整体数值更小。比较小数时，**逐位从高位到低位比较**，而非直接比较小数位数的长短。"
     ]
    }
   ],
   "source": [
    "reasoning_content = \"\"  # 定义完整思考过程\n",
    "answer_content = \"\"     # 定义完整回复\n",
    "is_answering = False   # 判断是否结束思考过程并开始回复\n",
    "\n",
    "# 创建聊天完成请求\n",
    "completion = client.chat.completions.create(\n",
    "    model=\"qwq-32b\",  # 此处以 qwq-32b 为例，可按需更换模型名称\n",
    "    messages=[\n",
    "        {\"role\": \"user\", \"content\": \"9.9和9.11谁大\"}\n",
    "    ],\n",
    "    stream=True,\n",
    "    # 解除以下注释会在最后一个chunk返回Token使用量\n",
    "    # stream_options={\n",
    "    #     \"include_usage\": True\n",
    "    # }\n",
    ")\n",
    "\n",
    "print(\"\\n\" + \"=\" * 20 + \"思考过程\" + \"=\" * 20 + \"\\n\")\n",
    "\n",
    "for chunk in completion:\n",
    "    # 如果chunk.choices为空，则打印usage\n",
    "    if not chunk.choices:\n",
    "        print(\"\\nUsage:\")\n",
    "        print(chunk.usage)\n",
    "    else:\n",
    "        delta = chunk.choices[0].delta\n",
    "        # 打印思考过程\n",
    "        if hasattr(delta, 'reasoning_content') and delta.reasoning_content != None:\n",
    "            print(delta.reasoning_content, end='', flush=True)\n",
    "            reasoning_content += delta.reasoning_content\n",
    "        else:\n",
    "            # 开始回复\n",
    "            if delta.content != \"\" and is_answering is False:\n",
    "                print(\"\\n\" + \"=\" * 20 + \"完整回复\" + \"=\" * 20 + \"\\n\")\n",
    "                is_answering = True\n",
    "            # 打印回复过程\n",
    "            print(delta.content, end='', flush=True)\n",
    "            answer_content += delta.content"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 如果你对多模态大模型感兴趣，可以参考：\n",
    "\n",
    "    * [视觉理解](https://help.aliyun.com/zh/model-studio/user-guide/vision)\n",
    "    * [音频理解](https://help.aliyun.com/zh/model-studio/user-guide/audio-language-model)\n",
    "    * [全模态](https://help.aliyun.com/zh/model-studio/user-guide/qwen-omni)\n",
    "    * [文生图](https://help.aliyun.com/zh/model-studio/user-guide/text-to-image)\n",
    "    * [视频生成](https://help.aliyun.com/zh/model-studio/user-guide/video-generation)\n",
    "## 🔥 课后小测验\n",
    "【单选题】 2.1.1. 以下代码片段的作用是什么？\n",
    "```python\n",
    "os.environ[\"OPENAI_API_KEY\"] = \"your-api-key-here\"\n",
    "```\n",
    "A. 从磁盘加载API密钥。<br>\n",
    "B. 将API密钥存储到内存中。<br>\n",
    "C. 将API密钥设置为环境变量。<br>\n",
    "D. 创建一个新的 API 密钥。<br>\n",
    "参考答案：C<br>\n",
    "\n",
    "【案例分析题】2.1.2 小明正在使用通义千问开发一个写作助手应用。根据本节课程学习的大模型工作原理和参数调节知识，请分析以下场景并给出解决方案：\n",
    "场景A：每次让模型写一篇关于“人工智能发展”的文章时，生成的内容都非常相似，缺乏创意性。\n",
    "场景B：让模型写一份技术文档时，生成的内容经常偏离主题，加入了一些不相关的内容。\n",
    "请问：\n",
    "1. 基于本节课程中学习的大模型工作流程，这两个场景的问题产生的原因可能是什么？\n",
    "2. 应该如何调整 temperature 或 top_p 参数来解决这些问题？\n",
    "\n",
    "参考答案：\n",
    "场景A：\n",
    "原因：temperature 值设置过低，导致模型总是倾向于选择概率最高的 token，使生成内容缺乏多样性。\n",
    "解决方案：适当提高 temperature 值(如从0.3提高到0.7-0.9)，让模型在生成时有更多创意空间。\n",
    "场景B：\n",
    "原因：temperature 值可能设置过高，或 top_p 值过大，使模型可以选择概率较低的 token，导致内容偏离主题。\n",
    "解决方案：降低 temperature 值（如从1.2降到0.5-0.7），或将 top_p 设置在0.7-0.9之间，以保持内容的相关性和专业性。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ✅评价反馈\n",
    "欢迎你参与[阿里云大模型ACP课程问卷](https://survey.aliyun.com/apps/zhiliao/Mo5O9vuie) 反馈学习体验和课程评价。\n",
    "你的批评和鼓励都是我们前进的动力！"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "learnacp",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
